﻿namespace SpikingAnalyticsLib

open System.IO
open SpikingNeuronLib
open SpikingAnalyticsLib.NetworkUtilities
open SpikingAnalyticsLib.GraphUtilities

// ===============================================================================================================
// PNGDescriptor: describes a PNG in terms of pairs of linked firing events
// A convenience type for use with the VanillaAPI

type PNGDescriptor (firstFiringTime:int, lastFiringTime:int, linkedEvents:SpikingNeuronLib.LinkedEventPair[]) =
    inherit SpikingNeuronLib.PNGDescriptor(linkedEvents)

    let CreatePostEventMap (linkedEventPairs:LinkedEventPair list) =
        linkedEventPairs
        |> Seq.fold (fun (state:Map<int * int, (int * int) list>) eventPair ->
            let key = eventPair.PreEvent.Time, eventPair.PreEvent.NeuronIndex
            let eventList =
                if state.ContainsKey key then
                    state.[key]
                else
                    []
            Map.add key ((eventPair.PostEvent.Time, eventPair.PostEvent.NeuronIndex) :: eventList) state
        ) Map.empty

    let GetMatchedEventPairs (eventSet:Set<int * int>) (linkedEvents:seq<SpikingNeuronLib.LinkedEventPair>) =
        linkedEvents
        |> Seq.map (fun eventPair ->
            (eventPair.PreEvent.Time, eventPair.PreEvent.NeuronIndex),
            (eventPair.PostEvent.Time, eventPair.PostEvent.NeuronIndex), eventPair)
        |> Seq.filter (fun (preEvent, postEvent, _) -> eventSet.Contains preEvent && eventSet.Contains postEvent)
        |> Seq.map (fun (_, _, eventPair) -> eventPair)
        |> Seq.toList

    let GetLastLayer (linkedEvents:seq<SpikingNeuronLib.LinkedEventPair>) =
        let lastEventPair =
            linkedEvents
            |> Seq.maxBy (fun eventPair -> eventPair.Layer)
        lastEventPair.Layer

    let SaveToFile (savePath:string) (descriptor:PNGDescriptor) =
        use sw = File.CreateText(savePath)
        let header = sprintf "%d %d" descriptor.FirstFiringTime descriptor.LastFiringTime
        sw.WriteLine(header)
        descriptor.LinkedEventsAsTuples
        |> Seq.iter (fun ((time1, nindex1), (time2, nindex2), weight, probability) ->
                let line = sprintf "%d %d %d %d %f %f" time1 nindex1 time2 nindex2 weight probability
                sw.WriteLine(line)
            )

    member this.FirstFiringTime = firstFiringTime

    member this.LastFiringTime = lastFiringTime
    
    member this.LinkedEvents:SpikingNeuronLib.LinkedEventPair[] = linkedEvents

    member this.LinkedEventsAsList =
        linkedEvents
        |> Array.toList

    member this.LinkedEventsAsTuples =
        linkedEvents
        |> Seq.map (fun linkedEventPair ->
            (linkedEventPair.PreEvent.Time, linkedEventPair.PreEvent.NeuronIndex),
            (linkedEventPair.PostEvent.Time, linkedEventPair.PostEvent.NeuronIndex),
            linkedEventPair.ConnectionWeight, linkedEventPair.LinkProbability
        )

    /// Return the last layer i.e. the linked event pair with the largest value for layer
    // May return zero as the layer is not always set
    member this.LastLayer = GetLastLayer linkedEvents

    /// Return a set of the neurons that participate in this group
    member this.Neurons =
        linkedEvents
        |> Seq.map (fun eventPair -> seq { yield eventPair.PreEvent; yield eventPair.PostEvent } )
        |> Seq.concat
        |> Seq.map (fun ev -> ev.NeuronIndex)
        |> Set.ofSeq

    /// Return a transposition mapping that compresses the neuron indexes
    // (see also the Compress() method)
    member this.CompressMapping =
        this.Neurons
        |> Seq.mapi (fun index neuron -> neuron, index)
        |> Map.ofSeq

    /// Return the longest path through the network structure
    member this.LongestPath =
        // create a pre-post event map i.e. a list of postsynaptic events for each presynaptic event
        let prePostEventMap = CreatePostEventMap this.LinkedEventsAsList

        [ 0; 1; 2; ]    // try each trigger event
        |> Seq.map (fun index ->
            let anchorEvent = linkedEvents.[index].PreEvent.Time, linkedEvents.[index].PreEvent.NeuronIndex
            GetLongestEventPath anchorEvent prePostEventMap
        )
        |> Seq.maxBy (fun path -> path.Length)

    /// Return a map of paired firing events i.e. for each pre event, map the list of post events
    member this.PrePostEventMap =
        this.LinkedEventsAsTuples
        |> Seq.fold (fun (state:Map<int * int, (int * int) list>) (preEvent, postEvent, connectionWeight, probability) ->
            let eventList =
                if state.ContainsKey preEvent then
                    state.[preEvent]
                else
                    []
            Map.add preEvent (postEvent :: eventList) state
        ) Map.empty

    new(firstFiringTime:int, lastFiringTime:int, linkedEventTuples:((int * int) * (int * int) * float * float) list) =
        let linkedEvents =
            linkedEventTuples
            |> Seq.map (fun ((time1, nindex1), (time2, nindex2), weight, probability) ->
                let preEvent = new FiringEvent(time1, nindex1, EventLabel.Foreground)
                let postEvent = new FiringEvent(time2, nindex2, EventLabel.Foreground)
                new LinkedEventPair(preEvent, postEvent, weight, probability)
            )
            |> Seq.toList
        new PNGDescriptor(firstFiringTime, lastFiringTime, linkedEvents)

    // Create a descriptor from the specified list of linked events
    new(linkedEvents:SpikingNeuronLib.LinkedEventPair list) =
        let minTime, maxTime =
            let timeList =
                linkedEvents
                |> Seq.map (fun linkedEventPair ->
                    (linkedEventPair.PreEvent.Time, linkedEventPair.PreEvent.NeuronIndex),
                    (linkedEventPair.PostEvent.Time, linkedEventPair.PostEvent.NeuronIndex),
                    linkedEventPair.ConnectionWeight, linkedEventPair.LinkProbability
                )
                |> Seq.map (fun ((t1, n1), (t2, n2), w, p) ->
                        seq {
                            yield t1
                            yield t2
                        }
                    )
                |> Seq.concat
                |> Seq.toList
            Seq.min timeList, Seq.max timeList
        new PNGDescriptor(minTime, maxTime, linkedEvents)

    new(firstFiringTime:int, lastFiringTime:int, linkedEvents:SpikingNeuronLib.LinkedEventPair list) =
        let linkedEventsArray = List.toArray linkedEvents
        new PNGDescriptor(firstFiringTime, lastFiringTime, linkedEventsArray)

    // Load the data from the specified file
    new(loadPath) =
        let firstFiringTime, lastFiringTime, linkedEvents = ParseLinkedEvents (File.ReadLines(loadPath))
        new PNGDescriptor(firstFiringTime, lastFiringTime, linkedEvents)

    // Load the data from the specified lines of strings
    new(lines) =
        let firstFiringTime, lastFiringTime, linkedEvents = ParseLinkedEvents lines
        new PNGDescriptor(firstFiringTime, lastFiringTime, linkedEvents)

    // Return the set of linked event pairs whose events match the firing specified event set
    member this.MatchWithEvents(eventSet:Set<int * int>) = GetMatchedEventPairs eventSet linkedEvents

    // Return the longest path that starts from one of the firing events in the triggering pattern
    member this.GetLongestPath(trigger:Pattern) =

        // explore all possible paths (from each of the stimulus pattern events) and return the longest path
        [ 0; 1; 2; ]    // try each trigger event
        |> Seq.map (fun index ->
            let anchorEvent =
                let selectedEvent = trigger.EventList.[index]
                selectedEvent.Time, selectedEvent.NeuronIndex
            GetLongestEventPath anchorEvent this.PrePostEventMap
        )
        |> Seq.maxBy (fun path -> path.Length)

    /// Return a new PNGDescriptor where each neuron is transposed by the specified mapping
    // The mapping must map each neuron index in the descrptor to a new index
    // (see also the Compress() method)
    member this.Transpose(mapping:Map<int, int>) =

        let TransposeEvent (ev:FiringEvent) =
            new FiringEvent(ev.Time, mapping.[ev.NeuronIndex], ev.Label)

        let transposedEventPairs =
            linkedEvents
            |> Seq.map (fun eventPair ->
                let preEvent = TransposeEvent eventPair.PreEvent
                let postEvent = TransposeEvent eventPair.PostEvent
                new LinkedEventPair(preEvent, postEvent, eventPair.ConnectionWeight, eventPair.ConnectionDelay, eventPair.Layer, eventPair.LinkProbability)
            )
            |> Seq.toList

        new PNGDescriptor(firstFiringTime, lastFiringTime, transposedEventPairs)

    /// Return a new PNGDescriptor with the neuron indexes compressed into the range 0..N-1
    /// where N is the number of neurons that participate in the group
    /// (see the CompressMapping property for the compression mapping used in this method)
    // (see also the Neurons property and the Transpose(mapping) method)
    member this.Compress () =

        let mapping = this.CompressMapping
        let descriptor = this.Transpose(mapping)

        descriptor

    member this.GetMinMax(desiredRange) =
        let AdjustValue value isUpper =
            let modifier = if isUpper then 1 else 0
            ((value / desiredRange) + modifier) * desiredRange
        AdjustValue firstFiringTime false, AdjustValue lastFiringTime true

    member this.Save(savePath:PathDescriptor) =
        if savePath = null then
            failwith "Invalid path descriptor"
        SaveToFile (savePath.Path("txt")) this

    member this.Save(savePath:string) =
        if not (Directory.Exists(Path.GetDirectoryName(savePath))) then
            failwith "Folder does not exist"
        SaveToFile savePath this

    override this.ToString() =
        sprintf "PNGDescriptor: time range: %d - %d; Size: %d" firstFiringTime lastFiringTime linkedEvents.Length
